﻿using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using Nvigorate.Common;
using Nvigorate.Common.Reflection;
using Nvigorate.Data.Query;
using Nvigorate.Relational.Builders;
using Nvigorate.Relational.Mapping;
using Nvigorate.Relational.Mapping.Index;
using Nvigorate.Relational.Mapping.Interface;
using Nvigorate.Relational.Repository;
using Nvigorate.Relational.State;
using Nvigorate.TestObjects;

namespace Nvigorate.Test.Relational.Repository
{
    [TestClass]
    public class UpdateBuilderTests
    {
        #region Fields

        private const string INSERT_INTO_PERSON_TEMP_TABLE_QUERY = "InsertIntoPersonTempTable";
        private const string PERSON_VARIABLE_LIST_VARIABLES = "PersonVariableAssignments";
        private const string VEHICLE_VARIABLE_LIST_VARIABLES = "VehicleVariableAssignments";
        private const string PERSON_OUTPUT_CLAUSE = "PersonOutputClause";

        #endregion

        #region AssignmentFilter tests

        [TestMethod]
        public void AssignmentFilter_PropertyIsDirty_ReturnsTrue()
        {
            // Arrange
            var sourceMap = GetValidPersonMap();
            var keyManager = GetKeyManager();
            var repositoryMapIndex = new Mock<IRepositoryMapIndex>();
            var person = CreatePerson("123456789", "John");
            person.FirstName = "Dirty";
            var builder = new UpdateBuilderHarness(sourceMap, keyManager, PersistenceDecorator.Create(person), null);

            // Assert
            Assert.IsTrue(builder.IsInAssignmentFilter(sourceMap.Assignments[1]));
        }

        [TestMethod]
        public void AssignmentFilter_PropertyIsNotPersistable_ReturnsFalse()
        {
            // Arrange
            var sourceMap = GetValidPersonMap();
            sourceMap.Assignments[0].Persistable = false;
            var keyManager = GetKeyManager();
            var repositoryMapIndex = new Mock<IRepositoryMapIndex>();
            var person = CreatePerson("123456789", null);

            var builder = new UpdateBuilderHarness(sourceMap, keyManager, PersistenceDecorator.Create(person), null);

            // Assert
            Assert.IsFalse(builder.IsInAssignmentFilter(sourceMap.Assignments[1]));
        }

        [TestMethod]
        public void AssignmentFilter_PropertyIsPrimaryKey_ReturnsFalse()
        {
            // Arrange
            var sourceMap = GetValidPersonMap();
            var keyManager = GetKeyManager();
            var repositoryMapIndex = new Mock<IRepositoryMapIndex>();
            var person = CreatePerson("123456789", null);

            var builder = new UpdateBuilderHarness(sourceMap, keyManager, PersistenceDecorator.Create(person), null);

            // Assert
            Assert.IsTrue(sourceMap.Assignments[0].PrimaryKey);
            Assert.IsFalse(builder.IsInAssignmentFilter(sourceMap.Assignments[0]));
        }

        #endregion

        #region Build Tests
        
        [TestMethod]
        public void Build_InstanceHasNoChanges_ReturnsInsertIntoKeyTable()
        {
            // Arrange
            var sourceMap = GetValidPersonMap();
            var keyManager = GetKeyManager();
            var repositoryMapIndex = new Mock<IRepositoryMapIndex>();
            var person = CreatePerson("123456789", null);

            var builder = new UpdateBuilderHarness(sourceMap, keyManager, PersistenceDecorator.Create(person), null);

            // Act
            var insert = builder.Build(false) as QueryList;

            // Assert
            Assert.AreEqual(INSERT_INTO_PERSON_TEMP_TABLE_QUERY, ((MessageQueryObject)insert[0]).Value);
        }

        [TestMethod]
        public void Build_InstanceHasChanges_ReturnsVariableAssignments()
        {
            // Arrange
            var sourceMap = GetValidPersonMap();
            var keyManager = GetKeyManager();
            var person = CreatePerson("123456789", "Test First");
            person.FirstName = "First Name";

            var repositoryMapIndex = new Mock<IRepositoryMapIndex>(); 
            var builder = new UpdateBuilderHarness(sourceMap, keyManager, PersistenceDecorator.Create(person), null);

            // Act
            var query = builder.Build(false) as QueryList;

            // Assert
            Assert.IsNotNull(query);
            Assert.AreEqual(PERSON_VARIABLE_LIST_VARIABLES, ((MessageQueryObject)query[0]).Value);
        }

        [TestMethod]
        public void Build_WithDirtyProperties_IncludesOnlyDirtyPropertiesInUpdateValues()
        {
            // Arrange
            var sourceMap = GetValidPersonMap();
            var keyManager = GetKeyManager();
            var person = CreatePerson("123456789", "Test First");
            person.FirstName = "First Name";
            var repositoryMapIndex = new Mock<IRepositoryMapIndex>();
            var builder = new UpdateBuilderHarness(sourceMap, keyManager, PersistenceDecorator.Create(person), null);

            // Act
            var query = builder.Build(false) as QueryList;

            // Assert
            Assert.IsNotNull(query);
            var update = query[1] as Update;
            Assert.IsNotNull(update);
            Assert.AreEqual(1, update.ColumnList.Count);
            Assert.AreEqual("firstName", ((AssignmentColumn)update.ColumnList[0]).Name);
        }

        [TestMethod]
        public void Build_WithDirtyProperties_UsesAssignmentFilterToBuildUpdateValues()
        {
            // Arrange
            var sourceMap = GetValidPersonMap();
            var keyManager = GetKeyManager();
            var person = CreatePerson("123456789", "Test First");
            person.FirstName = "First Name";
            var repositoryMapIndex = new Mock<IRepositoryMapIndex>();
            var builder = new UpdateBuilderHarness(sourceMap, keyManager, PersistenceDecorator.Create(person), null);
            Func<IAssignment, bool> filter = a => false;

            // Act
            var defaultResult = builder.Build(false) as QueryList;
            builder.SetAssignmentFilter(filter);
            var filteredResult = builder.Build(false) as QueryList;

            // Assert
            Assert.IsNotNull(defaultResult);
            Assert.IsNotNull(filteredResult);
            Assert.AreEqual(1, ((Update)defaultResult[1]).ColumnList.Count);
            Assert.AreEqual(1, ((Update)filteredResult[1]).ColumnList.Count);
        }

        [TestMethod]
        public void Build_WithConcurrencyOff_CreatesWhereClauseContainingOnlyThePrimaryKey()
        {
            // Arrange
            var sourceMap = GetValidPersonMap();
            var keyManager = GetKeyManager();
            var person = CreatePerson("123456789", "Test First");
            person.FirstName = "First Name";
            var repositoryMapIndex = new Mock<IRepositoryMapIndex>();
            var builder = new UpdateBuilderHarness(sourceMap, keyManager, PersistenceDecorator.Create(person), null);

            // Act
            var query = builder.Build(false) as QueryList;

            // Assert
            Assert.IsNotNull(query);
            var update = query[1] as Update;
            Assert.IsNotNull(update);
            Assert.AreEqual(1, update.Qualifiers.Count);
            var where = update.Qualifiers[0] as Where;
            Assert.AreEqual(1, where.SearchCriteria.Count);
            QueryAssert.EqualCriterion(where.SearchCriteria[0].Criteria[0], "SSN", person.SocialSecurity);
        }

        [TestMethod]
        public void Build_WithConcurrencyOn_CreateWhereClauseContainingAllMappedFields()
        {
            // Arrange
            var sourceMap = GetValidPersonMap();
            var keyManager = GetKeyManager();
            var defaultFirstName = "Test First";
            var person = CreatePerson ("123456789", defaultFirstName);
            person.FirstName = "First Name";
            var repositoryMapIndex = new Mock<IRepositoryMapIndex>();
            var builder = new UpdateBuilderHarness(sourceMap, keyManager, PersistenceDecorator.Create(person), null);

            // Act
            var query = builder.Build(true) as QueryList;

            // Assert
            Assert.IsNotNull(query);
            var update = query[1] as Update;
            Assert.IsNotNull(update);
            Assert.AreEqual(1, update.Qualifiers.Count);
            var where = update.Qualifiers[0] as Where;
            Assert.AreEqual(1, where.SearchCriteria.Count);
            QueryAssert.EqualCriterion(where.SearchCriteria[0].Criteria[0],"SSN", person.SocialSecurity);
            QueryAssert.EqualCriterion(where.SearchCriteria[0].Criteria[1], "firstName", defaultFirstName);
        }

        [TestMethod]
        public void Build_WithDirtyProperties_IncludesOutputClause()
        {
            // Arrange
            var sourceMap = GetValidPersonMap();
            var keyManager = GetKeyManager();
            var person = CreatePerson("123456789", "Test First");
            person.FirstName = "First Name";
            var repositoryMapIndex = new Mock<IRepositoryMapIndex>();
            var builder = new UpdateBuilderHarness(sourceMap, keyManager, PersistenceDecorator.Create(person), null);

            // Act
            var query = builder.Build(false) as QueryList;

            // Assert
            Assert.IsNotNull(query);
            Assert.AreEqual(2, query.Count);
            var output = ((Update) query[1]).Output;
            Assert.AreEqual(PERSON_OUTPUT_CLAUSE, ((MessageQueryObject)output.ColumnList[0]).Value);
        }

        [TestMethod]
        public void Build_WithConcurrencyOff_DoesNotIncludeConcurrencyCheck()
        {
            // Arrange
            var sourceMap = GetValidPersonMap();
            var keyManager = GetKeyManager();
            var person = CreatePerson("123456789", "Test First");
            person.FirstName = "First Name";
            var repositoryMapIndex = new Mock<IRepositoryMapIndex>();
            var builder = new UpdateBuilderHarness(sourceMap, keyManager, PersistenceDecorator.Create(person), null);

            // Act
            var query = builder.Build(false) as QueryList;

            // Assert
            Assert.IsNotNull(query);
            Assert.AreEqual(2, query.Count);            
        }

        [TestMethod]
        public void Build_WithConcurrencyOn_RaisesErrorForConcurrencyFailure()
        {
            // Arrange
            var sourceMap = GetValidPersonMap();
            var keyManager = GetKeyManager();
            var person = CreatePerson("123456789", "Test First");
            person.FirstName = "First Name";
            var repositoryMapIndex = new Mock<IRepositoryMapIndex>();
            var builder = new UpdateBuilderHarness(sourceMap, keyManager, PersistenceDecorator.Create(person), null);

            // Act
            var query = builder.Build(true) as QueryList;

            // Assert
            Assert.IsNotNull(query);
            Assert.AreEqual(3, query.Count);
            IfBlock check = query[2] as IfBlock;
            Assert.IsNotNull(check);
            QueryAssert.RaiseErrorEquals(check.ActionIfTrue, 14, 14, sourceMap.SourceName);
        }

        [TestMethod]
        public void Build_WithConcurrencyOn_UsesRowcountZeroAsConcurrencyCheck()
        {
            // Arrange
            var sourceMap = GetValidPersonMap();
            var keyManager = GetKeyManager();
            var person = CreatePerson("123456789", "Test First");
            person.FirstName = "First Name";
            var repositoryMapIndex = new Mock<IRepositoryMapIndex>();
            var builder = new UpdateBuilderHarness(sourceMap, keyManager, PersistenceDecorator.Create(person), null);

            // Act
            var query = builder.Build(true) as QueryList;

            // Assert
            Assert.IsNotNull(query);
            Assert.AreEqual(3, query.Count);
            IfBlock check = query[2] as IfBlock;
            Assert.IsNotNull(check);
            QueryAssert.EqualCriterion(check.Criteria[0].Criteria[0], 0, "@@ROWCOUNT");
        }
        #endregion

        #region Private Methods

        private Person CreatePerson(string ssn, string firstName)
        {
            var person = new Person {SocialSecurity = ssn, FirstName = firstName};
            person.InstanceState.ClearDirtyState();
            return person;
        }

        private ISourceMap GetValidPersonMap()
        {
            var sourceMap = new SourceMap("Person") {SourceName = "Person"};
            sourceMap.Assignments.Add(new MemberAssignment(
                                          new AssignmentPoint("Person", "SSN"),
                                          new AssignmentPoint("Person", "SocialSecurity")) { PrimaryKey = true});
            sourceMap.Assignments.Add(new MemberAssignment(
                                          new AssignmentPoint("Person", "firstName"),
                                          new AssignmentPoint("Person", "FirstName")));

            return sourceMap;
        }

        private ISourceMap GetValidVehicleMap()
        {
            var sourceMap = new SourceMap("Vehicle") {SourceName = "Vehicle"};
            sourceMap.Assignments.Add(new MemberAssignment(true, "Vehicle", "VehicleId", "Vehicle", "VIN") { PrimaryKey = true });
            sourceMap.Assignments.Add(new MemberAssignment(false, "Vehicle", "OwnerSSN", "Vehicle", "OwnerSocial"));
            return sourceMap;
        }

        private IPersistenceKeyManager GetKeyManager()
        {
            var keyManager = new Mock<IPersistenceKeyManager>();
            keyManager.Expect(
                km => km.GetKeyVariable(typeof (Person), It.Is<IAssignment>(a => a.Source.Field == "SSN"))).
                Returns("key1");

            keyManager.Expect(
                km => km.GetVariableAssignments(typeof(Person), It.Is<ISourceMap>(m => m.SourceName == "Person"),
                                                It.IsAny<IPersistable>())).Returns(ReturnValue(PERSON_VARIABLE_LIST_VARIABLES));

            keyManager.Expect(
                km => km.GetKeyVariable(typeof (Vehicle), It.Is<IAssignment>(a => a.Source.Field == "OwnerSSN"))).Returns("key2");

            keyManager.Expect(
                km => km.GetVariableAssignments(typeof(Vehicle), It.Is<ISourceMap>(m => m.SourceName == "Vehicle"),
                                                It.IsAny<IPersistable>())).Returns(ReturnValue(VEHICLE_VARIABLE_LIST_VARIABLES));

            keyManager.Expect(km => km.GetInsertIntoKeyTable(It.Is<IPersistable>(p => p.Instance is Person))).Returns(
                ReturnValue(INSERT_INTO_PERSON_TEMP_TABLE_QUERY));

            keyManager.Expect(km => km.GetOutputClauseForSource(It.Is<IPersistable>(p => p.Instance is Person),
                                                                It.Is<ISourceMap>(m => m.SourceName == "Person"),
                                                                "INSERTED")).Returns(new Output(ReturnValue(PERSON_OUTPUT_CLAUSE),
                                                                    Table.From("Person")));
            return keyManager.Object;
        }

        #endregion

        #region Nested Types

        private class UpdateBuilderHarness : UpdateBuilder
        {
            public UpdateBuilderHarness(ISourceMap sourceMap, IPersistenceKeyManager keyManager, IPersistable target,
                                        IPersistable parent) : base(sourceMap, keyManager, target, parent)
            {
            }

            public bool IsInAssignmentFilter(IAssignment assignment)
            {
                return _assignmentFilter.Invoke(assignment);
            }

            public void SetAssignmentFilter(Func<IAssignment, bool> filter)
            {
                _assignmentFilter = filter;
            } 
        }

        private QueryList ReturnValue(string value)
        {
            return new QueryList().Add(new MessageQueryObject(value));
        }

        private void SetConcurrencyProperties(ITrackState instance)
        {
            foreach (var tuple in instance.InstanceState.ChangedValues)
            {
                tuple.Value2 = Reflector.Read(instance, tuple.Value1);
            }
        }
        #endregion
    }
}